iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
自我挑戰組

攜手 AI 從零開始打造一款 Flutter 應用程式系列 第 7

Day 7: App 的整體造型師 - 使用 ThemeData 打造專屬風格

  • 分享至 

  • xImage
  •  

前言

大家好!經過昨天的努力,我們的「省錢拍拍」App 已經擁有一個可以流暢滾動的動態列表了。功能上邁進了一大步,但視覺上,它看起來仍像一個「樣板 App」——滿滿的預設色彩,缺乏品牌個性。

如果我們手動去修改每一個 Text 的顏色、每一個 Icon 的大小,那將會是一場災難。幸運的是,Flutter 提供了一套強大的中央樣式系統:ThemeData

學習如何從一個「種子顏色」生成和諧的色彩系統,並使用 google_fonts 套件,無需下載任何字體檔,就能為 App 打造一套獨一無二、貫穿全局的視覺主題。

Step 1: ThemeData 在哪裡?

ThemeData 是我們 App 全局樣式的設定檔。它就位於 lib/main.dart 中,MyApp Widget 的 build 方法裡,作為 MaterialApptheme 屬性。

// lib/main.dart -> MyApp -> build
@override
Widget build(BuildContext context) {
  return MaterialApp(
    // ...
    // 就是它!App 的風格都由這裡定義
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      useMaterial3: true,
    ),
    home: HomePage(),
  );
}

目前,它只用了一行 ColorScheme.fromSeed(seedColor: Colors.deepPurple) 來定義顏色。這就是我們著手改造的起點。

Step 2: 現代主題的核心 - ColorScheme.fromSeed

在 Material 3 的設計規範中,App 的色彩由一組語意化的 ColorScheme (色彩方案) 驅動。它包含 primary (主要色)、secondary (次要色)、surface (表面色)、background (背景色) 等。

ColorScheme.fromSeed() 是一個非常聰明的建構子:你只需要給它一個「種子顏色」,Flutter 就會自動為你生成一整套完整、和諧、且包含淺色與深色模式的 ColorScheme

我們希望「省錢拍拍」App 的主題色能給人一種清新、穩定的感覺,因此選擇綠色系的 Colors.teal 作為種子顏色。

修改 ThemeData:

// lib/main.dart -> MyApp -> build -> theme
theme: ThemeData(
  // 只需要改變這個種子顏色
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), // 從 deepPurple 改為 teal
  useMaterial3: true,
),

Hot Restart 後,App 的主色調就會立刻更新。

Step 3: 注入靈魂 - 使用 google_fonts 套件

過去,在 Flutter 中使用自訂字體需要手動下載字體檔、建立資料夾、並在 pubspec.yaml 中註冊,過程繁瑣。現在,有了 google_fonts 套件,一切都變得無比簡單。

  1. 新增 google_fonts 依賴
    打開你的 pubspec.yaml 檔案,在 dependencies 區塊下,加入 google_fonts
# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  
  # ... 其他套件 ...

  # 新增這一行
  google_fonts: ^6.3.1 # 建議使用最新版本

修改完 pubspec.yaml 後,VS Code 通常會自動執行 flutter pub get。如果沒有,請手動在終端機執行它,或者點擊右上角的 "Get Packages" 按鈕。

  1. 全局應用字體
    回到 lib/main.dart 首先在文件頂部導入 google_fonts 套件:
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; // 導入 google_fonts

接著,修改 ThemeData。我們不再使用 fontFamily 屬性,而是直接將 textTheme 替換為 google_fonts 提供的版本。這裡我們選用 Noto Sans TC (思源黑體-繁體中文)。

// lib/main.dart -> MyApp -> build -> theme
theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
  useMaterial3: true,

  // 舊方法是使用 fontFamily,現在我們用這個更強大的方式
  textTheme: GoogleFonts.notoSansTCTextTheme(
    Theme.of(context).textTheme,
  ),
),

程式碼解析:

  • GoogleFonts.notoSansTCTextTheme(): 這是套件提供的輔助函式,它會自動從網路下載 Noto Sans TC 字體(並在設備上快取),然後建立一個完整的 TextTheme (包含 headline, body, title 等所有文字樣式)。
  • Theme.of(context).textTheme: 我們將 App 的原始 textTheme 傳入,google_fonts 會聰明地將新字體應用在原始的樣式設定上(例如顏色、大小),而不是完全覆蓋。

Step 4: 細部風格調整

情況一:全局樣式已滿足需求

當我們嘗試讓首頁總支出金額的文字變大、更突出時,可能會遇到一個編譯錯誤。如果你直接在 const Column 內使用 Theme.of(context),編譯器會報錯。

// lib/main.dart -> HomePage -> build -> 總覽區塊
// ...
Text(
  'NT\$ 12,345',
  // 這裡的 headlineLarge 樣式已經自動套用了 Noto Sans TC 字體
  style: Theme.of(context).textTheme.headlineLarge,
),
// ...
  • 這裡會出錯的原因是,當我們寫 const Column(...),等於在告訴編譯器,這個 Column 及其所有子元件的內容在編譯時就已經完全確定,程式運行時無需再重新計算,可以進行效能優化。
  • Theme.of(context):這是一個運行時的方法。它會沿著 Widget 樹向上查找當前 context 下的 ThemeData。因為 context 本身就是一個運行時的物件,所以這個操作的結果不可能是編譯時期的常數。
  • 衝突:當我們在 const Column 的子元件 Text 中,使用了 Theme.of(context) 這個運行時的值,就破壞了 const 的約定。

修改 HomePagebuild 方法中的總覽區塊:

// lib/main.dart -> HomePage -> build -> 總覽區塊
// ...
Container(
  padding: const EdgeInsets.all(24.0),
  margin: const EdgeInsets.all(16.0),
  decoration: BoxDecoration(
    color: Colors.grey.shade200,
    borderRadius: BorderRadius.circular(10),
  ),
  // 【重要觀念修正】: 因為下方的 Text Widget 使用了 Theme.of(context),
  // 它是一個執行時的值,所以這裡的 Column 不能是 const。
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      const Text(
        '本月總支出',
        style: TextStyle(fontSize: 16, color: Colors.black54),
      ),
      Text(
        'NT\$ 12,345',
        // 使用 Theme.of(context) 來取得當前主題的樣式
        style: Theme.of(context).textTheme.headlineLarge?.copyWith(
          color: Colors.teal.shade700, // 我們還可以在獲取的基礎上微調
        ),
      ),
    ],
  ),
),
// ...

Day7

情況二:需要單獨使用特定字體或樣式
假設在某個特殊頁面,你想使用不同的字體,或者只是想微調某個 Text 的樣式,也可以直接呼叫 GoogleFonts

// 一個單獨使用的範例
Text(
  '特殊標語',
  style: GoogleFonts.lato( // 直接指定使用 Lato 字體
    fontSize: 24,
    fontStyle: FontStyle.italic,
    color: Colors.pink,
  ),
),

今日結語

今天我們為「省錢拍拍」進行了一次徹底且現代化的「形象改造」。學會了:

  1. 使用 ColorScheme.fromSeed 快速建立一套和諧的色彩系統。
  2. 如何整合 google_fonts 套件,無需管理字體檔,就能優雅地為 App 設定全局字體。
  3. 全局設定 (textTheme) 與局部設定 (GoogleFonts.lato()) 的使用時機。

我們的 App 現在外觀與功能兼備,但它仍然是一個只能「看」的 App。從明天開始,我們將正式進入「互動」的領域:學習如何處理使用者輸入,並打造新增消費紀錄的表單頁面


上一篇
Day 6: 讓畫面動起來 - ListView 與 Card 的列表魔法
下一篇
Day 8: 從「能看」到「能用」- 處理使用者輸入與 Form 表單
系列文
攜手 AI 從零開始打造一款 Flutter 應用程式8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言